/**
 * File TextOverlay.java
 * ---------------------------------------------------------
 *
 * Copyright (C) 2012 David Bauske (david.bauske@googlemail.com)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 * - The origin of the software must not be misrepresented.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 * TL;DR: As long as you clearly give me credit for this Software, you are free to use as you like, even in commercial software, but don't blame me
 * 		if it breaks something.
 */
package net.cl.util.ui;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.geom.RoundRectangle2D;
import java.util.ArrayList;
import java.util.List;

import javax.swing.SwingUtilities;

import net.cl.util.math.MathUtil;

/**
 * Displays a configurable box of text. This box can be placed in a corner of any component
 * that provides {@link PaintListener} events.
 *
 * @author David
 */
public class TextOverlay extends PaintAdapter
{
	private static final int	DEFAULT_H_PADDING			= 8;
	private static final int	DEFAULT_V_PADDING			= 8;
	private static final int	DEFAULT_CORNER_HEIGHT		= 10;
	private static final Color	DEFAULT_BACKGROUND_COLOR	= Color.BLACK;
	private static final float	DEFAULT_BACKGROUND_OPACITY	= 0.5f;
	private static final double	FONT_BASELINE_FACTOR		= (2.0 / 3.0);
	private static final double	FONT_BASELINE_REST_FACTOR	= 1.0 - FONT_BASELINE_FACTOR;

	protected List<TextRow>		mRows						= new ArrayList<TextRow>();
	protected Color				mDefaultColor				= Color.BLACK;
	protected Font				mDefaultFont				=
			new Font("sans", Font.BOLD, 12);

	protected Corner			mCorner						= Corner.TOP_LEFT;

	int							mHPadding					= DEFAULT_H_PADDING;
	int							mVPadding					= DEFAULT_V_PADDING;
	int							mCornerHeight				= DEFAULT_CORNER_HEIGHT;
	protected Color				mBackgroundColor			= DEFAULT_BACKGROUND_COLOR;
	protected float				mBackgroundOpacity			= DEFAULT_BACKGROUND_OPACITY;

	/**
	 * Adds a new TextRow to the overlay.
	 *
	 * @param pNewRow The row to add. May not be null.
	 * @throws NullPointerException if pNewRow is null.
	 */
	public void addRow(TextRow pNewRow)
	{
		if(pNewRow == null)
		{
			throw new NullPointerException("pNewRow may not be null.");
		}

		if(pNewRow.mColor == null)
		{
			pNewRow.setColor(this.mDefaultColor);
		}
		if(pNewRow.mFont == null)
		{
			pNewRow.setFont(this.mDefaultFont);
		}

		this.mRows.add(pNewRow);
	}

	public void removeRow(TextRow pRow)
	{
		this.mRows.remove(pRow);
	}

	/**
	 * Sets a row in the overlay. The row index isn't required to exist yet.
	 *
	 * @param pIndex The zero-based index of the row to replace. Must be greater or equal to zero.
	 * @param pRow The row to set. Must be non-null.
	 * @throws IndexOutOfBoundsException if <tt>pIndex &lt; 0</tt>.
	 * @throws
	 */
	public void setRow(int pIndex, TextRow pRow)
	{
		if(pIndex < 0)
		{
			throw new IndexOutOfBoundsException("pIndex may not be < 0.");
		}
		if(pRow == null)
		{
			throw new NullPointerException("pRow may not be null.");
		}

		// Fill previously non-existing indices with empty objects.
		for(int i = this.mRows.size(); i < pIndex; ++i)
		{
			this.mRows.add(new TextRow());
		}

		this.mRows.set(pIndex, pRow);
	}

	/**
	 * @return The default Color of this TextOverlay object
	 */
	public Color getDefaultColor()
	{
		return mDefaultColor;
	}

	/**
	 * @param pDefaultColor the default Color to set
	 */
	public void setDefaultColor(Color pDefaultColor)
	{
		this.mDefaultColor = pDefaultColor;
	}

	/**
	 * @return The default Font of this TextOverlay object
	 */
	public Font getDefaultFont()
	{
		return mDefaultFont;
	}

	/**
	 * @param pDefaultFont the default Font to set
	 */
	public void setDefaultFont(Font pDefaultFont)
	{
		this.mDefaultFont = pDefaultFont;
	}

	/**
	 * @return The corner in which this will be painted.
	 */
	public Corner getCorner()
	{
		return mCorner;
	}

	/**
	 * @param pCorner the corner to paint this in.
	 */
	public void setCorner(Corner pCorner)
	{
		this.mCorner = pCorner;
	}

	@Override
	public void onPaintEnd(Graphics2D pGraphics, Component pComp)
	{
		// Compute the dimensions of the overlay box
		Dimension size = this.getSize(pGraphics);

		// Delegate the painting work to the Corner object.
		BackgroundMetrics metrics = this.mCorner.computeBackgroundMetrics(size, this, pComp.getSize());

		// Create the background object
		RoundRectangle2D background = new RoundRectangle2D.Double(metrics.x, metrics.y,
				metrics.w, metrics.h,
				this.mCornerHeight, this.mCornerHeight);

		PaintMode mode = PaintMode.FOREGROUND;
		{
			// If mouse is inside the overlay switch to background render mode.
			Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
			SwingUtilities.convertPointFromScreen(mouseLoc, pComp);
			if(background.contains(mouseLoc))
			{
				mode = PaintMode.BACKGROUND;
			}
		}

		float bgOpacity = mode.computeBackgroundOpacity(this.mBackgroundOpacity);

		// Draw the background
		{
			Composite originalComposite = pGraphics.getComposite();
			pGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, bgOpacity));
			pGraphics.setColor(this.mBackgroundColor);
			pGraphics.fill(background);
			pGraphics.setComposite(originalComposite);
		}

		int curYPos = (int) (this.mCorner.computeTopmostLinePosition(this) + metrics.y);

		for(TextRow row : this.mRows)
		{
			FontMetrics fm = pGraphics.getFontMetrics(row.mFont);
			int width = fm.stringWidth(row.mText);
			int x = (int) (this.mCorner.computeLineStart(width, size.width, this) + metrics.x);
			curYPos += fm.getHeight() * FONT_BASELINE_FACTOR;

			float fontOpacity = mode.computeFontOpacity();
			Composite originalComposite = pGraphics.getComposite();
			pGraphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, fontOpacity));
			pGraphics.setColor(row.mColor);
			pGraphics.setFont(row.mFont);
			pGraphics.drawString(row.mText, x, curYPos);
			pGraphics.setComposite(originalComposite);

			curYPos += fm.getHeight() * FONT_BASELINE_REST_FACTOR;
		}
	}

	/**
	 * Computes the overlay's size if it would be painted using
	 * the specified Graphics object.
	 *
	 * @param pGraphics The graphics object that provides the necessary sizing information.
	 * @return The size of the overlay (including the padding).
	 */
	public Dimension getSize(Graphics2D pGraphics)
	{
		int size = this.mRows.size();
		int[] widths = new int[size];
		int height = 2 * this.mVPadding;

		for(int i = 0; i < size; ++i)
		{
			TextRow row = this.mRows.get(i);
			FontMetrics fm = pGraphics.getFontMetrics(row.getFont());

			widths[i] = fm.stringWidth(row.getText());
			height += fm.getHeight();
		}

		int width = MathUtil.max(widths) + 2 * this.mHPadding;

		return new Dimension(width, height);
	}

	// ------------------------------------------------------------------------------------
	// (Package-) Protected methods
	// ------------------------------------------------------------------------------------

	// ------------------------------------------------------------------------------------
	// Inner Types
	// ------------------------------------------------------------------------------------

	/**
	 * Represents one row of text in a TextOverlay. Each line can be individually colored.
	 */
	public static class TextRow
	{
		private String	mText;
		private Color	mColor;
		private Font	mFont;

		public TextRow()
		{
			this.mText = "";
			this.mColor = Color.BLACK;
		}

		public TextRow(String pText)
		{
			this.mText = pText;
			this.mColor = Color.BLACK;
		}

		public TextRow(String pText, Color pColor)
		{
			this.mText = pText;
			this.mColor = pColor;
		}

		/**
		 * @return The text of this TextRow object
		 */
		public String getText()
		{
			return mText;
		}

		/**
		 * @param pText the text to set
		 */
		public void setText(String pText)
		{
			this.mText = pText;
		}

		/**
		 * @return The color of this TextRow object
		 */
		public Color getColor()
		{
			return mColor;
		}

		/**
		 * @param pColor the color to set
		 */
		public void setColor(Color pColor)
		{
			this.mColor = pColor;
		}

		/**
		 * @return The font of this TextRow object
		 */
		public Font getFont()
		{
			return this.mFont;
		}

		/**
		 * @param pFont the font to set
		 */
		public void setFont(Font pFont)
		{
			this.mFont = pFont;
		}

		@Override
		public String toString()
		{
			return "[" + this.mColor + "] " + this.mText;
		}

		@Override
		public boolean equals(Object pOther)
		{
			if(pOther instanceof TextRow)
			{
				TextRow other = (TextRow) pOther;
				return this.mColor.equals(other.mColor) &&
						this.mText.equals(other.mText) &&
						this.mFont.equals(other.mFont);
			}
			else
			{
				return false;
			}
		}

		@Override
		public int hashCode()
		{
			int result = 17;
			result = 31 * result + this.mColor.hashCode();
			result = 31 * result + this.mText.hashCode();
			result = 31 * result + this.mFont.hashCode();
			return result;
		}
	}

	/**
	 * Enumeration of the available corners in a standard rectangle.
	 */
	public enum Corner
	{
		TOP_LEFT
		{
			@Override
			protected BackgroundMetrics computeBackgroundMetrics(Dimension pSize, TextOverlay pOverlay, Dimension pComponentSize)
			{
				BackgroundMetrics metrics = new BackgroundMetrics(
						-pOverlay.mCornerHeight,
						-2 * pOverlay.mCornerHeight,
						pSize.width + 2 * pOverlay.mCornerHeight,
						pSize.height + pOverlay.mCornerHeight);
				return metrics;
			}

			@Override
			protected int computeLineStart(int pStringWidth, int pOverlayWidth, TextOverlay pOverlay)
			{
				return 2 * pOverlay.mCornerHeight;
			}

			@Override
			protected int computeTopmostLinePosition(TextOverlay pOverlay)
			{
				return 3 * pOverlay.mVPadding;
			}
		},
		TOP_RIGHT
		{
			@Override
			protected BackgroundMetrics computeBackgroundMetrics(Dimension pSize, TextOverlay pOverlay, Dimension pComponentSize)
			{
				BackgroundMetrics metrics = new BackgroundMetrics(
						pComponentSize.getWidth() - pSize.width,
						-2 * pOverlay.mCornerHeight,
						pSize.width + 2 * pOverlay.mCornerHeight,
						pSize.height + pOverlay.mCornerHeight);
				return metrics;
			}

			@Override
			protected int computeLineStart(int pStringWidth, int pOverlayWidth, TextOverlay pOverlay)
			{
				return pOverlayWidth - pStringWidth - pOverlay.mCornerHeight;
			}

			@Override
			protected int computeTopmostLinePosition(TextOverlay pOverlay)
			{
				return 3 * pOverlay.mVPadding;
			}
		},
		BOTTOM_RIGHT
		{
			@Override
			protected BackgroundMetrics computeBackgroundMetrics(Dimension pSize, TextOverlay pOverlay, Dimension pComponentSize)
			{
				BackgroundMetrics metrics = new BackgroundMetrics(
						pComponentSize.getWidth() - pSize.width,
						pComponentSize.getHeight() - pSize.height + pOverlay.mCornerHeight,
						pSize.width + 2 * pOverlay.mCornerHeight,
						pSize.height + pOverlay.mCornerHeight);
				return metrics;
			}

			@Override
			protected int computeLineStart(int pStringWidth, int pOverlayWidth,
					TextOverlay pOverlay)
			{
				return pOverlayWidth - pStringWidth - pOverlay.mCornerHeight;
			}

			@Override
			protected int computeTopmostLinePosition(TextOverlay pOverlay)
			{
				return pOverlay.mVPadding;
			}
		},
		BOTTOM_LEFT
		{
			@Override
			protected BackgroundMetrics computeBackgroundMetrics(Dimension pSize, TextOverlay pOverlay, Dimension pComponentSize)
			{
				BackgroundMetrics metrics = new BackgroundMetrics(
						-pOverlay.mCornerHeight,
						pComponentSize.getHeight() - pSize.height + pOverlay.mCornerHeight,
						pSize.width + 2 * pOverlay.mCornerHeight,
						pSize.height + pOverlay.mCornerHeight);
				return metrics;
			}

			@Override
			protected int computeLineStart(int pStringWidth, int pOverlayWidth, TextOverlay pOverlay)
			{
				return 2 * pOverlay.mCornerHeight;
			}

			@Override
			protected int computeTopmostLinePosition(TextOverlay pOverlay)
			{
				return pOverlay.mVPadding;
			}
		};

		protected abstract BackgroundMetrics computeBackgroundMetrics(Dimension pSize, TextOverlay pOverlay, Dimension pComponentSize);
		protected abstract int computeLineStart(int pStringWidth, int pOverlayWidth, TextOverlay pOverlay);
		protected abstract int computeTopmostLinePosition(TextOverlay pOverlay);
	}

	public static class BackgroundMetrics
	{
		public double		x;
		public double		y;
		public double		w;
		public double		h;

		public BackgroundMetrics(double x, double y, double w, double h)
		{
			this.x = x;
			this.y = y;
			this.w = w;
			this.h = h;
		}
	}

	private enum PaintMode
	{
		FOREGROUND
		{
			@Override
			public float computeBackgroundOpacity(float pBackgroundOpacity)
			{
				return pBackgroundOpacity;
			}

			@Override
			public float computeFontOpacity()
			{
				return 1.0f;
			}
		},
		BACKGROUND
		{
			@Override
			public float computeBackgroundOpacity(float pBackgroundOpacity)
			{
				return pBackgroundOpacity * 0.2f;
			}

			@Override
			public float computeFontOpacity()
			{
				return 0.2f;
			}
		};

		public abstract float computeBackgroundOpacity(float pBackgroundOpacity);
		public abstract float computeFontOpacity();
	}
}
